Skip to content

fix(server): start event bus processor for manual wiring - #997

Merged
ehsavoie merged 4 commits into
a2aproject:mainfrom
014-code:fix/995-main-event-bus-processor-startup
Sep 25, 2026
Merged

ehsavoie merged 4 commits into
a2aproject:mainfrom
014-code:fix/995-main-event-bus-processor-startup

Conversation

@014-code

@014-code 014-code commented Jul 25, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Expose synchronized, idempotent MainEventBusProcessor.start() for CDI and manual wiring.
  • Start the supplied processor from DefaultRequestHandler construction so manual setups process and persist events.
  • Keep startup safe when CDI @PostConstruct and explicit wiring both call start().
  • Cover builder-based manual wiring with a regression test.
  • Document the CDI constructor and add a manual wiring example using separate agent and event-consumer executors.

Related Issue

This fixes #995.

Verification

  • mvn -pl server-common -am -Dtest=DefaultRequestHandlerTest#testBuilderStartsManuallyConstructedMainEventBusProcessor -Dsurefire.failIfNoSpecifiedTests=false test — passed (1 test).
  • mvn -pl server-common -am -DskipTests test-compile — passed.

@guofengzh

Copy link
Copy Markdown

Based on the summary in #997, I adjusted the way Spring beans are constructed in ServerConfig, then, the expected response was received.

The change are:

  1. Use the constructor of DefaultRequestHandler to construct an instance of DefaultRequestHandler.
  2. Construct a separate MainEventBusProcessor bean, and then inject it into the DefaultRequestHandler bean.

In other words, the following construct

	@Bean
	public RequestHandler requestHandler(AgentExecutor agentExecutor, TaskStore taskStore, QueueManager queueManager,
	                                     PushNotificationConfigStore pushConfigStore,
										 MainEventBus mainEventBus,
										 PushNotificationSender pushSender,
	                                     @Qualifier("a2aInternal") Executor executor) {
		ExecutorService eventConsumerExecutor = Executors.newFixedThreadPool(3);
		return new DefaultRequestHandler(agentExecutor, taskStore, queueManager, pushConfigStore,
				new MainEventBusProcessor(mainEventBus, taskStore, pushSender, queueManager),
				executor,
				eventConsumerExecutor);
	}

is transformed into

	@Bean
	public MainEventBusProcessor mainEventBusProcessor(MainEventBus mainEventBus,TaskStore taskStore,
													   PushNotificationSender pushSender,
													   QueueManager queueManager) {
		return new MainEventBusProcessor(mainEventBus, taskStore, pushSender, queueManager);
	}
	

	@Bean
	public RequestHandler requestHandler(AgentExecutor agentExecutor, TaskStore taskStore, QueueManager queueManager,
	                                     PushNotificationConfigStore pushConfigStore,
	                                     MainEventBusProcessor mainEventBusProcessor,
	                                     @Qualifier("a2aInternal") Executor executor) {
		return new DefaultRequestHandler(agentExecutor, taskStore, queueManager,
				pushConfigStore, mainEventBusProcessor,
				executor, executor);
	}

Spring Boot recognizes @PostConstruct (and @Inject), so DefaultRequestHandler.initConfig() and MainEventBusProcessor.start() will be executed in this case.

Then, test using the following request:

{
  "jsonrpc": "2.0",
  "id": "6e7bf51a-f364-42d7-a060-343da3cc4f43",
  "method": "SendMessage",
  "params": {
    "message": {
      "messageId": "messageId-1",
      "role": "ROLE_USER",
      "parts": [
        {
          "text": "Can you check systems?"
        }
      ]
    }
  }
}

Response 200 OK

Response Body:

{
  "error": null,
  "id": "6e7bf51a-f364-42d7-a060-343da3cc4f43",
  "jsonrpc": "2.0",
  "result": {
    "role": "ROLE_AGENT",
    "parts": [
      {
        "text": "Hello World",
        "metadata": null
      }
    ],
    "messageId": "d062faba-39e6-4ac6-b891-9e0ba1363a69",
    "contextId": "c2c16e07-dfad-40f1-958c-41d336d4363e",
    "taskId": "d38c2bc5-7491-4920-a290-93582d21a9e0",
    "referenceTaskIds": null,
    "metadata": null,
    "extensions": null
  }
}

That is, a success result was returned.

Hopefully, these new improvements will be helpful to your revisions.

Thanks for what you did.

@ehsavoie

ehsavoie commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Can you please rebase ? Also an example on how you are using this would be great since it is in SpringBoot runtime

@kabir kabir left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @014-code! Overall it looks good, but I think exposing the start() method directly will be simpler.

@SuppressWarnings("NullAway.Init")
@PostConstruct
void start() {
synchronized void start() {

@kabir kabir Aug 5, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can just make this public synchronized

*/
public void ensureStarted() {
// Method intentionally empty - just forces proxy resolution
start();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please undo this change

// I am unsure about the correct scope.
// Also reworked to make a Supplier since otherwise the builder gets polluted with wrong tasks
this.requestContextBuilder = () -> new SimpleRequestContextBuilder(taskStore, false);
this.mainEventBusProcessor.ensureStarted();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just call mainEventBusProcessor.start() if we have to.
But I wonder, since this comes in as a constructor parameter, if you could just not call start() on it before calling the constructor? So this line doesn't seem essential to me.

}
eventQueue.enqueueEvent(message);
Message messageToSend = message;
if (message.taskId() == null || message.contextId() == null) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude thinks:

The null-ID backfill in AgentEmitter (lines 522-528 of the diff) is unrelated to the event bus processor startup fix.
The PR's own test doesn't exercise this path — the test executor calls sendMessage(String), not sendMessage(Message).
This should be a separate PR with its own issue reference.

@kabir kabir left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@014-code do you plan on looking into this?

014-code and others added 3 commits September 25, 2026 13:47
Ensure MainEventBusProcessor.ensureStarted() starts manually constructed processors, and call it from DefaultRequestHandler.create(...). This prevents manually wired integrations from leaving events stuck on the main event bus.

Add a regression test for the manual wiring path that returns a Message response.

This fixes a2aproject#995
Start the main event bus processor from the public request handler constructor and verify the direct manual wiring path.\n\nThis fixes a2aproject#995
@kabir
kabir force-pushed the fix/995-main-event-bus-processor-startup branch from 6a59823 to 4b6eac7 Compare September 25, 2026 12:50
@kabir

kabir commented Sep 25, 2026

Copy link
Copy Markdown
Collaborator

I have updated the PR with my comments and rebased on main

@ehsavoie
ehsavoie merged commit 7e73cab into a2aproject:main Sep 25, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Help]: Could not find a Task/Message for 84ced8e6-3705-41c6-9158-f2b880463a32

4 participants